package ch.ethz.fcl.metrobuzz.render.techniques.circos;

import javax.vecmath.Vector2d;

import ch.ethz.fcl.metrobuzz.tools.MBTools;

/**
 * Dealing with arc elements overlapping issues
 * 
 * @author Zeng Wei
 * @email zeng@arch.ethz.ch
 * 
 */
public class CircosDirection {
	// records the direction for each element
	private Vector2d[] initDirs;
	private Vector2d[] repelDirs;
	CircosMatrix data;

	public CircosDirection(CircosMatrix data) {
		this.data = data;
		initDirs = new Vector2d[data.size()];
		// in case users forget setting the directions
		float angle = (float) Math.PI * 2 / (data.size() - 1);
		Vector2d xDir = new Vector2d(1, 0);
		for (int i = 0; i < data.size(); i++) {
			initDirs[i] = MBTools.rotate(xDir, angle * i);
		}
	}

	public void setDirection(int index, Vector2d dir) {
		if (index < 0 || index >= data.size())
			return;

		initDirs[index] = dir;
	}

	public Vector2d[] getInitialDirections() {
		return initDirs;
	}

	public Vector2d[] getRepelDirections() {
		return repelDirs;
	}

	/**
	 * This method returns the rotated directions of circos data such that no
	 * arc elements overlap each other
	 * 
	 * @param minGap
	 *            The minmum angular gap between two consecutive arc elements
	 * @param totalArcAngle
	 *            Summarization of all arc angles. Maximum should be 180 degree.
	 * @param minRenderAngle
	 *            Minmum angular degree of arc element to be rendered
	 * @return
	 */
	public Vector2d[] getNonOverlapDirections(float minGap,
			float totalArcAngle, float minRenderAngle) {
		if (totalArcAngle + minGap * (data.size() - 1) >= 2 * Math.PI) {
			return initDirs;
		}

		repelDirs = new Vector2d[data.size()];

		for (int i = 0; i < data.size(); i++)
			repelDirs[i] = new Vector2d(initDirs[i].x, initDirs[i].y);

		int[] renderOrder = data.getRenderOrder();
		float[] arcAngles = new float[data.size()];

		for (int i = 0; i < renderOrder.length; i++) {
			int index = renderOrder[i];
			arcAngles[index] = totalArcAngle
					* (data.getIncoming()[index] + data.getOutgoing()[index])
					/ (data.getTotalSum() - (data.getIncoming()[0] + data
							.getOutgoing()[0]));
		}

		for (int i = 0; i < renderOrder.length; i++) {
			evaluateDirection(repelDirs, i, arcAngles, minRenderAngle, minGap);
		}

		return repelDirs;
	}

	private void evaluateDirection(Vector2d[] dirs, int index,
			float[] arcAngles, float minRenderAngle, float minGap) {
		int order = getOrderIndex(index, arcAngles, minRenderAngle, true);

		int[] renderOrder = data.getRenderOrder();
		int preIndex = (index + renderOrder.length + 1) % renderOrder.length;
		int nextIndex = (index + renderOrder.length - 1) % renderOrder.length;

		int preOrder = getOrderIndex(preIndex, arcAngles, minRenderAngle, false);
		int nextOrder = getOrderIndex(nextIndex, arcAngles, minRenderAngle,
				true);

		// System.out.println("order " + order + " pre Order " + preOrder + " "
		// + arcAngles[order] + " " + arcAngles[preOrder] + " min gap "
		// + minGap);
		if (order != preOrder
				&& checkOverlap(dirs, order, preOrder, arcAngles, minGap)) {
			rotateDirctions(dirs, order, preOrder, arcAngles, minGap);
			evaluateDirection(dirs, index, arcAngles, minRenderAngle, minGap);
			evaluateDirection(dirs, preIndex, arcAngles, minRenderAngle, minGap);
		}

		if (order != nextOrder && nextOrder != preOrder
				&& checkOverlap(dirs, order, nextOrder, arcAngles, minGap)) {
			rotateDirctions(dirs, order, nextOrder, arcAngles, minGap);
			evaluateDirection(dirs, index, arcAngles, minRenderAngle, minGap);
			evaluateDirection(dirs, nextIndex, arcAngles, minRenderAngle,
					minGap);
		}
	}

	/**
	 * get the render order index of arc elements
	 * 
	 * @param i
	 * @param arcAngles
	 * @param minRenderAngle
	 * @return
	 */
	private int getOrderIndex(int i, float[] arcAngles, float minRenderAngle,
			boolean increase) {
		int[] renderOrder = data.getRenderOrder();
		// make sure the index is within the order
		int index = (i + renderOrder.length) % renderOrder.length;

		int order = renderOrder[index];
		int count = 0;
		// make sure this arc element will be rendered (larger than minimum
		// render angle)
		while (arcAngles[order] < minRenderAngle && count < renderOrder.length) {
			if (increase)
				index = (index + renderOrder.length + 1) % renderOrder.length;
			else
				index = (index + renderOrder.length - 1) % renderOrder.length;
			order = renderOrder[index];
			count++;
		}

		return order;
	}

	private boolean checkOverlap(Vector2d[] dirs, int order, int neighborOrder,
			float[] arcAngles, float minGap) {
		// angular gap between two directions, result lies in (-PI, PI)
		float angularGap = (float) MBTools.angle(dirs[order],
				dirs[neighborOrder]);
		if (angularGap == 0)
			return false;

		if (Float.isNaN(arcAngles[order])
				|| Float.isNaN(arcAngles[neighborOrder]))
			return false;

		if (arcAngles[order] == 0 || arcAngles[neighborOrder] == 0)
			return false;

		// minimum angular gap between the centeral directions of arc
		// elements
		float minDirGap = minGap + arcAngles[order] / 2
				+ arcAngles[neighborOrder] / 2;
		// this angular gap must be large enough to make it non-overlap
		if (Math.abs(angularGap) >= minDirGap)
			return false;
		else
			return true;
	}

	private void rotateDirctions(Vector2d[] dirs, int order, int neighborOrder,
			float[] arcAngles, float minGap) {
		// angular gap between two directions, result lies in (-PI, PI)
		float angularGap = (float) MBTools.angle(dirs[order],
				dirs[neighborOrder]);
		if (angularGap == 0)
			return;

		if (arcAngles[order] == 0 || arcAngles[neighborOrder] == 0)
			return;

		// minimum angular gap between the centeral directions of arc
		// elements
		float minDirGap = minGap + arcAngles[order] / 2
				+ arcAngles[neighborOrder] / 2;
		// this angular gap must be large enough to make it non-overlap
		if (Math.abs(angularGap) < minDirGap) {
			float rotate = minDirGap - Math.abs(angularGap);

			if (angularGap >= 0) { // anti clockwise from this to pre
				dirs[order] = MBTools.rotate(dirs[order], -rotate / 2);
				dirs[neighborOrder] = MBTools.rotate(dirs[neighborOrder],
						rotate / 2);
			} else {
				dirs[order] = MBTools.rotate(dirs[order], rotate / 2);
				dirs[neighborOrder] = MBTools.rotate(dirs[neighborOrder],
						-rotate / 2);
			}
		}
	}
}
